iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 27
0
自我挑戰組

寫遊戲初體驗系列 第 27

Day 27 Particle System 3

  • 分享至 

  • xImage
  •  

Particle System update

我們的 Particle System 會更新我們 Particle 的所以物理,以及幫助我們將粒子繪製出來。

class ParticleSystem {

public:
    
    ParticleSystem() = default;
    
    void update(entt::registry &registry, float dt);
    void render(entt::registry &registry);
    void init(std::shared_ptr<Shader> shader);
    void initEmitter(entt::registry &registry, entt::entity &entity, EmitData data);

private:

    std::shared_ptr<Shader> shader;
    std::shared_ptr<VertexArray> vertexArray;
    uint32_t vao;
}

所以我們的 ParticleSystem 會有基本的一些更新及渲染的方法。所以這裡存著我封裝的 Shader 以及我封裝的 vertexArray。

在遊戲進入循環之前,我們要先將我們的 Particle System 選染所要用的東西都先初始化好不然會出事。

void ParticleSystem::init(std::shared_ptr<Shader> shader) {

    this->shader = shader;
    GLfloat particle_quad[] = {
            0.0f, 1.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 0.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            1.0f, 1.0f, 1.0f, 1.0f,
            1.0f, 0.0f, 1.0f, 0.0f
    };

    vertexArray = std::make_shared<VertexArray>();
    vertexArray->bind();

    std::shared_ptr<VertexBuffer> vertexBuffer;
    vertexBuffer = std::make_shared<VertexBuffer>(particle_quad, sizeof(particle_quad));

    BufferLayout layout = {

            {ShaderDataType::Float4, "vertex"}
    };

    vertexBuffer->setLayout(layout);
    vertexArray->addVertexBuffer(vertexBuffer);
    vertexArray->unbind();
}

進入遊戲循環之後呢,我們會按下鍵盤上的按鍵,隨後從 Json 檔案裡面讀入資料並輸入給 Emitter。Particle System 負責幫助輸入資料。

void ParticleSystem::initEmitter(entt::registry &registry, entt::entity &entity, EmitData data) {

    auto &emitter = registry.get<EmitterComponent>(entity);
    auto &transform = registry.get<TransformComponent>(entity);
    auto &data = emitter.data;

    // 將讀進來的設定檔案全部設置到emitter

    emitter.angleRange = data.angleRange;
    emitter.startSpeed = data.startSpeed;
    emitter.endSpeed = data.endSpeed;
    emitter.startSize = data.startSize;
    emitter.endSize = data.endSize;
    emitter.rotateSpeed = data.rotateSpeed;

    emitter.emitNumber = data.emitNumber;
    emitter.emitVariance = data.emitVariance;
    emitter.maxParticleLife = data.maxParticleLife;
    emitter.maxParticlesPerFrame = data.emitNumber + data.emitVariance;

    emitter.poolSize = emitter.maxParticlesPerFrame * (emitter.maxParticleLife + 1);
    emitter.particles.resize(emitter.poolSize);

    // Render Properties
    emitter.startColor = data.startColor;
    emitter.endColor = data.endColor;

    emitter.active = true;
    emitter.lifeTime = emitter.life = data.life;
    emitter.sleepTime = data.sleepTime;

    emitter.rotSpeedRand = data.rotSpeedRand;
    emitter.startSpeedRand = data.startSpeedRand;
    emitter.endSpeedRand = data.endSpeedRand;
    emitter.emitVarianceRand = data.emitVarianceRand;
    emitter.startSizeRand = data.startSizeRand;
    emitter.endSizeRand = data.endSizeRand;
    emitter.lifeRand = data.lifeRand;

    emitter.disX = data.disX;
    emitter.disY = data.disY;
}

就是字面上把所有的資料給 Emitter。

有資料的 Emitter 就要開始模擬效果了。

void ParticleSystem::update(entt::registry &registry, float dt) {

    auto group = registry.view<ParticleComponent, TransformComponent>();
    for( auto entity : group){
    
        auto &emitter = registry.get<ParticleComponent>(entity);
        auto &transform = registry.get<TransformComponent>(entity);
        
        if (emitter.active) {

	            // cal the num that should respawn this frame
            emitter.emissionRate = (int)(emitter.emitNumber + emitter.emitVariance * randFloat(emitter.emitVarianceRand.x, emitter.emitVarianceRand.y));

            for(int i = 0 ; i < emitter.emissionRate ; i++) {

                // find the unused Particle
                int unusedParticle = firstUnusedParticle(registry, entity);
                // 激活他
                respawnParticle(emitter, transform, unusedParticle);
            }
        }
    }
}

首先我們找出所有有 ParticleComponent 的 Entity,並更新他們。

更新的時候,首先確認此 Emitter 是否活著(emitter.active)如果活著的話,計算出這一個 frame 要產生多少個 particle(emitter.emissionRate),然後激活emissionRate數量的粒子。

激活粒子方法呢,我們會在我們的 Particle pool 中,找到未被使用的粒子並將它激活。

uint32_t ParticleSystem::firstUnusedParticle(entt::registry &registry, entt::entity &entity) {

    auto &particle = registry.get<EmitterComponent>(entity);
    for(int i = particle.lastUnusedParticle ; i < particle.poolSize ; i++) {

        if(particle.particles[i].life <= 0) {

            particle.lastUnusedParticle = i;
            return i;
        }
    }

    for(int i = 0 ; i < particle.lastUnusedParticle ; i++) {

        if(particle.particles[i].life <= 0) {

            particle.lastUnusedParticle = i;
            return i;
        }
    }

    particle.lastUnusedParticle = 0;
    return particle.lastUnusedParticle;
}

激活就是將 Emitter 的數據給每顆 Particle

void EmitterSystem::respawnParticle(EmitterComponent& emitter, TransformComponent& transform, int unusedParticle) {

    // 用random計算讓particle看起來不會太整齊
    float tmpStartSpeed = emitter.startSpeed * randFloat(emitter.startSpeedRand.x, emitter.startSpeedRand.y);
    float tmpEndSpeed = emitter.endSpeed * randFloat(emitter.endSpeedRand.x, emitter.endSpeedRand.y);
    float randAngle = randFloat(emitter.angleRange.x, emitter.angleRange.y);
    // random size
    float randStart = emitter.startSize * randFloat(emitter.startSizeRand.x, emitter.startSizeRand.y);
    float randEnd = emitter.endSize * randFloat(emitter.endSizeRand.x, emitter.endSizeRand.y);
    float randRadius = randFloat(randStart, randEnd/2);
    float randlife = emitter.maxParticleLife * randFloat(emitter.lifeRand.x, emitter.lifeRand.y);
    float randRotSpeed = emitter.rotateSpeed * randFloat(emitter.rotSpeedRand.x, emitter.rotSpeedRand.y);
    float randRotAngle = randFloat(0.f , emitter.rotateSpeed == 0 ? 0.f : 360.f);
    p2 dis = {randFloat(emitter.disX, -emitter.disX), randFloat(emitter.disY, -emitter.disY)};

    // Respawn Particle
    emitter.particles[unusedParticle].pos = transform.pos + dis; // depends on emitter's pos
    emitter.particles[unusedParticle].startVel.x = tmpStartSpeed * cos(DEG_2_RAD(randAngle));
    emitter.particles[unusedParticle].startVel.y = tmpStartSpeed * sin(DEG_2_RAD(randAngle));
    emitter.particles[unusedParticle].endVel.x = tmpEndSpeed * cos(DEG_2_RAD(randAngle));
    emitter.particles[unusedParticle].endVel.y = tmpEndSpeed * sin(DEG_2_RAD(randAngle));
    emitter.particles[unusedParticle].startRotSpeed = randRotSpeed;
    emitter.particles[unusedParticle].currentRotSpeed = randRotSpeed;
    emitter.particles[unusedParticle].angle = randRotAngle; // texture rotate

    // Life Properties
    emitter.particles[unusedParticle].life = emitter.particles[unusedParticle].startLife = randlife;
    emitter.particles[unusedParticle].currentSize = emitter.particles[unusedParticle].startSize = randRadius;
    emitter.particles[unusedParticle].endSize = emitter.endSize;
    emitter.particles[unusedParticle].t = 0.f;

    // Color Properties
    emitter.particles[unusedParticle].startColor = emitter.startColor;
    emitter.particles[unusedParticle].endColor = emitter.endColor;
}

如果有設定 sleep time 或是 emitter life time 記得隨時開關 emitter.active

for( auto entity : group){

    //[...]

    if(emitter.sleepTime > 0.f) {

        if(emitter.sleepTimer.read() <= emitter.sleepTime){

            emitter.active = false;
        }
        else {

            emitter.active = true;
            emitter.sleepTimer.start();
        }
    }
    // if emitter still has life
    if(emitter.life > 0.f) {
        // exceeded
        if(emitter.lifeTimer.read() >= emitter.life) {
            emitter.active = false;
        }
    }
}


接著有了數據的 Particle 就可以來更新了。


for( auto entity : group) {

    //[...]

    for(int i = 0 ; i < emitter.poolSize ; i++) {

        auto &particle = emitter.particles[i];
        if(particle.life > 0.f) {
            // Update
            // Interpolate values
            // 利用差質法算出目前的資訊
            particle.currentSize = interpolateBetweenRange(particle.startSize, particle.t, particle.endSize);
            particle.currentVel.x = interpolateBetweenRange(particle.startVel.x, particle.t, particle.endVel.x);
            particle.currentVel.y = interpolateBetweenRange(particle.startVel.y, particle.t, particle.endVel.y);
            particle.currentColor = RGBAinterpolation(particle.startColor, particle.t, particle.endColor);


            particle.pos.x += particle.currentVel.x * dt;
            particle.pos.y += particle.currentVel.y * dt;
            particle.life--;

            // 計算差值的time
            particle.t += (1.0f/(float)particle.startLife);
            if(particle.t >= 1.f)
                particle.t = 0.f;
            //

            // 計算rotate速度跟角度
            particle.currentRotSpeed += particle.startRotSpeed;
            particle.angle += dt * particle.currentRotSpeed;
            particle.angle = fmod(particle.angle, 360.f);
        }
    }
}

如果那顆粒子是活的,我們就更新他。
利用插值法更新速度、大小以及顏色。
然後根據速度以及這一個 frame 經過的時間更新位置
計算其他東西。

差值法

float ParticleSystem::interpolateBetweenRange(float min, float timestep, float max) {

    return min + (max - min) * timestep;
}

p4 ParticleSystem::RGBAinterpolation(p4 startColor, float timestep, p4 endColor) {

    p4 finColor;
    finColor.r = startColor.r + (endColor.r - startColor.r) * timestep;
    finColor.g = startColor.g + (endColor.g - startColor.g) * timestep;
    finColor.b = startColor.b + (endColor.b - startColor.b) * timestep;
    finColor.a = startColor.a + (endColor.a - startColor.a) * timestep;

    return finColor;
}

這樣就完成更新啦

明天就來畫出來吧!


上一篇
Day 26 Particle System 2
下一篇
Day 28 Particle System 4
系列文
寫遊戲初體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言